﻿using System;
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;
using UnityEngine.UI;

/// <summary>
/// 输入拓展组件，目前仅适用于聊天相关，支持emoji和@功能
/// </summary>
[AddComponentMenu("UI/Input Field Ex")]
public class InputFieldEx
    : Selectable,
    IUpdateSelectedHandler,
    IBeginDragHandler,
    IDragHandler,
    IEndDragHandler,
    IPointerClickHandler,
    ISubmitHandler,
    ICanvasElement,
    ILayoutElement
{
    // Setting the content type acts as a shortcut for setting a combination of InputType, CharacterValidation, LineType, and TouchScreenKeyboardType
    public enum ContentType
    {
        Standard,
        Autocorrected,
        IntegerNumber,
        DecimalNumber,
        Alphanumeric,
        Name,
        EmailAddress,
        Password,
        Pin,
        Custom
    }

    public enum InputType
    {
        Standard,
        AutoCorrect,
        Password,
    }

    public enum CharacterValidation
    {
        None,
        Integer,
        Decimal,
        Alphanumeric,
        Name,
        EmailAddress
    }

    public enum LineType
    {
        SingleLine,
        MultiLineSubmit,
        MultiLineNewline
    }

    public delegate char OnValidateInput(string text, int charIndex, char addedChar);

    [Serializable]
    public class SubmitEvent : UnityEvent<string> { }

    [Serializable]
    public class OnChangeEvent : UnityEvent<string> { }
    public class OnFocusEvent : UnityEvent<bool> { }

    protected TouchScreenKeyboard m_Keyboard;
    static private readonly char[] kSeparators = { ' ', '.', ',', '\t', '\r', '\n' };

    /// <summary>
    /// Text Text used to display the input's value.
    /// </summary>

    [SerializeField]
    [FormerlySerializedAs("text")]
    protected Text m_TextComponent;

    [SerializeField]
    protected Graphic m_Placeholder;

    [SerializeField]
    private ContentType m_ContentType = ContentType.Standard;

    /// <summary>
    /// Type of data expected by the input field.
    /// </summary>
    [FormerlySerializedAs("inputType")]
    [SerializeField]
    private InputType m_InputType = InputType.Standard;

    /// <summary>
    /// The character used to hide text in password field.
    /// </summary>
    [FormerlySerializedAs("asteriskChar")]
    [SerializeField]
    private char m_AsteriskChar = '*';

    /// <summary>
    /// Keyboard type applies to mobile keyboards that get shown.
    /// </summary>
    [FormerlySerializedAs("keyboardType")]
    [SerializeField]
    private TouchScreenKeyboardType m_KeyboardType = TouchScreenKeyboardType.Default;

    [SerializeField]
    private LineType m_LineType = LineType.SingleLine;

    /// <summary>
    /// Should hide mobile input.
    /// </summary>

    [FormerlySerializedAs("hideMobileInput")]
    [SerializeField]
    private bool m_HideMobileInput = false;

    /// <summary>
    /// What kind of validation to use with the input field's data.
    /// </summary>
    [FormerlySerializedAs("validation")]
    [SerializeField]
    private CharacterValidation m_CharacterValidation = CharacterValidation.None;

    /// <summary>
    /// Maximum number of characters allowed before input no longer works.
    /// </summary>
    [FormerlySerializedAs("characterLimit")]
    [SerializeField]
    private int m_CharacterLimit = 0;

    /// <summary>
    /// Event delegates triggered when the input field submits its data.
    /// </summary>
    [FormerlySerializedAs("onSubmit")]
    [FormerlySerializedAs("m_OnSubmit")]
    [FormerlySerializedAs("m_EndEdit")]
    [SerializeField]
    private SubmitEvent m_OnEndEdit = new SubmitEvent();

    /// <summary>
    /// Event delegates triggered when the input field changes its data.
    /// </summary>
    [FormerlySerializedAs("onValueChange")]
    [FormerlySerializedAs("m_OnValueChange")]
    [SerializeField]
    private OnChangeEvent m_OnValueChanged = new OnChangeEvent();
    public OnFocusEvent _mOnFocusChanged = new OnFocusEvent();

    /// <summary>
    /// Custom validation callback.
    /// </summary>
    [FormerlySerializedAs("onValidateInput")]
    [SerializeField]
    private OnValidateInput m_OnValidateInput;

    [SerializeField]
    private Color m_CaretColor = new Color(50f / 255f, 50f / 255f, 50f / 255f, 1f);

    [SerializeField]
    private bool m_CustomCaretColor = false;

    [FormerlySerializedAs("selectionColor")]
    [SerializeField]
    private Color m_SelectionColor = new Color(168f / 255f, 206f / 255f, 255f / 255f, 192f / 255f);

    /// <summary>
    /// Input field's value.
    /// </summary>

    [SerializeField]
    [FormerlySerializedAs("mValue")]
    protected string m_Text = string.Empty;

    [SerializeField]
    [Range(0f, 4f)]
    private float m_CaretBlinkRate = 0.85f;

    [SerializeField]
    [Range(1, 5)]
    private int m_CaretWidth = 1;

    [SerializeField]
    private bool m_ReadOnly = false;

    protected int m_CaretPosition = 0;
    protected int m_CaretSelectPosition = 0;
    private RectTransform caretRectTrans = null;
    protected UIVertex[] m_CursorVerts = null;
    private TextGenerator m_InputTextCache;
    private CanvasRenderer m_CachedInputRenderer;
    private bool m_PreventFontCallback = false;
    [NonSerialized]
    protected Mesh m_Mesh;
    private bool m_AllowInput = false;
    private bool m_ShouldActivateNextUpdate = false;
    private bool m_UpdateDrag = false;
    private bool m_DragPositionOutOfBounds = false;
    private const float kHScrollSpeed = 0.05f;
    private const float kVScrollSpeed = 0.10f;
    protected bool m_CaretVisible;
    private Coroutine m_BlinkCoroutine = null;
    private float m_BlinkStartTime = 0.0f;
    protected int m_DrawStart = 0;
    protected int m_DrawEnd = 0;
    private Coroutine m_DragCoroutine = null;
    private string m_OriginalText = "";
    private bool m_WasCanceled = false;
    private bool m_HasDoneFocusTransition = false;

    #region 拓展
    [SerializeField]
    bool m_isSupportEmoji = true;
    #endregion

    private BaseInput input
    {
        get
        {
            if(EventSystem.current && EventSystem.current.currentInputModule)
                return EventSystem.current.currentInputModule.input;
            return null;
        }
    }

    private string compositionString
    {
        get { return input != null ? input.compositionString : Input.compositionString; }
    }

    // Doesn't include dot and @ on purpose! See usage for details.
    const string kEmailSpecialCharacters = "!#$%&'*+-/=?^_`{|}~";

    protected InputFieldEx()
    {
        EnforceTextHOverflow();
    }

    protected Mesh mesh
    {
        get
        {
            if(m_Mesh == null)
                m_Mesh = new Mesh();
            return m_Mesh;
        }
    }

    protected TextGenerator cachedInputTextGenerator
    {
        get
        {
            if(m_InputTextCache == null)
                m_InputTextCache = new TextGenerator();

            return m_InputTextCache;
        }
    }

    /// <summary>
    /// Should the mobile keyboard input be hidden.
    /// </summary>

    public bool shouldHideMobileInput
    {
        set
        {
            SetPropertyUtility.SetStruct(ref m_HideMobileInput, value);
        }
        get
        {
            switch(Application.platform)
            {
                case RuntimePlatform.Android:
                case RuntimePlatform.IPhonePlayer:
                case RuntimePlatform.TizenPlayer:
                case RuntimePlatform.tvOS:
                    return m_HideMobileInput;
            }

            return true;
        }
    }

    bool shouldActivateOnSelect
    {
        get
        {
            return Application.platform != RuntimePlatform.tvOS;
        }
    }

    /// <summary>
    /// Input field's current text value.
    /// </summary>

    public string text
    {
        get
        {
            return m_Text;
        }
        set
        {
            if(this.text == value)
                return;
            if(value == null)
                value = "";
            value = value.Replace("\0", string.Empty); // remove embedded nulls
            if(m_LineType == LineType.SingleLine)
                value = value.Replace("\n", "").Replace("\t", "");

            // If we have an input validator, validate the input and apply the character limit at the same time.
            if(onValidateInput != null || characterValidation != CharacterValidation.None)
            {
                m_Text = "";
                OnValidateInput validatorMethod = onValidateInput ?? Validate;
                m_CaretPosition = m_CaretSelectPosition = value.Length;
                int charactersToCheck = characterLimit > 0 ? Math.Min(characterLimit, value.Length) : value.Length;
                for(int i = 0; i < charactersToCheck; ++i)
                {
                    char c = validatorMethod(m_Text, m_Text.Length, value[i]);
                    if(c != 0)
                        m_Text += c;
                }
            }
            else
            {
                m_Text = characterLimit > 0 && value.Length > characterLimit ? value.Substring(0, characterLimit) : value;
            }

#if UNITY_EDITOR
            if(!Application.isPlaying)
            {
                SendOnValueChangedAndUpdateLabel();
                return;
            }
#endif

            if(m_Keyboard != null)
                m_Keyboard.text = m_Text;

            if(m_CaretPosition > m_Text.Length)
                m_CaretPosition = m_CaretSelectPosition = m_Text.Length;
            else if(m_CaretSelectPosition > m_Text.Length)
                m_CaretSelectPosition = m_Text.Length;
            SendOnValueChangedAndUpdateLabel();
        }
    }

    public bool isFocused
    {
        get { return m_AllowInput; }
    }

    public float caretBlinkRate
    {
        get { return m_CaretBlinkRate; }
        set
        {
            if(SetPropertyUtility.SetStruct(ref m_CaretBlinkRate, value))
            {
                if(m_AllowInput)
                    SetCaretActive();
            }
        }
    }

    public int caretWidth { get { return m_CaretWidth; } set { if(SetPropertyUtility.SetStruct(ref m_CaretWidth, value)) MarkGeometryAsDirty(); } }

    public Text textComponent
    {
        get { return m_TextComponent; }
        set
        {
            if(SetPropertyUtility.SetClass(ref m_TextComponent, value))
                EnforceTextHOverflow();
        }
    }

    public Graphic placeholder { get { return m_Placeholder; } set { SetPropertyUtility.SetClass(ref m_Placeholder, value); } }

    public Color caretColor { get { return customCaretColor ? m_CaretColor : textComponent.color; } set { if(SetPropertyUtility.SetColor(ref m_CaretColor, value)) MarkGeometryAsDirty(); } }

    public bool customCaretColor { get { return m_CustomCaretColor; } set { if(m_CustomCaretColor != value) { m_CustomCaretColor = value; MarkGeometryAsDirty(); } } }

    public Color selectionColor { get { return m_SelectionColor; } set { if(SetPropertyUtility.SetColor(ref m_SelectionColor, value)) MarkGeometryAsDirty(); } }

    public SubmitEvent onEndEdit { get { return m_OnEndEdit; } set { SetPropertyUtility.SetClass(ref m_OnEndEdit, value); } }

    [Obsolete("onValueChange has been renamed to onValueChanged")]
    public OnChangeEvent onValueChange { get { return onValueChanged; } set { onValueChanged = value; } }

    public OnChangeEvent onValueChanged { get { return m_OnValueChanged; } set { SetPropertyUtility.SetClass(ref m_OnValueChanged, value); } }

    public OnFocusEvent onFocusChanged { get { return _mOnFocusChanged; } set { SetPropertyUtility.SetClass(ref _mOnFocusChanged, value); } }

    public OnValidateInput onValidateInput { get { return m_OnValidateInput; } set { SetPropertyUtility.SetClass(ref m_OnValidateInput, value); } }

    public int characterLimit { get { return m_CharacterLimit; } set { if(SetPropertyUtility.SetStruct(ref m_CharacterLimit, Math.Max(0, value))) UpdateLabel(); } }

    // Content Type related

    public ContentType contentType { get { return m_ContentType; } set { if(SetPropertyUtility.SetStruct(ref m_ContentType, value)) EnforceContentType(); } }

    public LineType lineType
    {
        get { return m_LineType; }
        set
        {
            if(SetPropertyUtility.SetStruct(ref m_LineType, value))
            {
                SetToCustomIfContentTypeIsNot(ContentType.Standard, ContentType.Autocorrected);
                EnforceTextHOverflow();
            }
        }
    }

    public InputType inputType { get { return m_InputType; } set { if(SetPropertyUtility.SetStruct(ref m_InputType, value)) SetToCustom(); } }

    public TouchScreenKeyboardType keyboardType { get { return m_KeyboardType; } set { if(SetPropertyUtility.SetStruct(ref m_KeyboardType, value)) SetToCustom(); } }

    public CharacterValidation characterValidation { get { return m_CharacterValidation; } set { if(SetPropertyUtility.SetStruct(ref m_CharacterValidation, value)) SetToCustom(); } }

    public bool readOnly { get { return m_ReadOnly; } set { m_ReadOnly = value; } }

    // Derived property
    public bool multiLine { get { return m_LineType == LineType.MultiLineNewline || lineType == LineType.MultiLineSubmit; } }
    // Not shown in Inspector.
    public char asteriskChar { get { return m_AsteriskChar; } set { if(SetPropertyUtility.SetStruct(ref m_AsteriskChar, value)) UpdateLabel(); } }
    public bool wasCanceled { get { return m_WasCanceled; } }

    protected void ClampPos(ref int pos)
    {
        if(pos < 0) pos = 0;
        else if(pos > text.Length) pos = text.Length;
    }

    /// <summary>
    /// Current position of the cursor.
    /// Getters are public Setters are protected
    /// </summary>

    protected int caretPositionInternal { get { return m_CaretPosition + compositionString.Length; } set { m_CaretPosition = value; ClampPos(ref m_CaretPosition); } }
    protected int caretSelectPositionInternal { get { return m_CaretSelectPosition + compositionString.Length; } set { m_CaretSelectPosition = value; ClampPos(ref m_CaretSelectPosition); } }
    private bool hasSelection { get { return caretPositionInternal != caretSelectPositionInternal; } }

#if UNITY_EDITOR
    [Obsolete("caretSelectPosition has been deprecated. Use selectionFocusPosition instead (UnityUpgradable) -> selectionFocusPosition", true)]
    public int caretSelectPosition { get { return selectionFocusPosition; } protected set { selectionFocusPosition = value; } }
#endif

    /// <summary>
    /// Get: Returns the focus position as thats the position that moves around even during selection.
    /// Set: Set both the anchor and focus position such that a selection doesn't happen
    /// </summary>

    public int caretPosition
    {
        get { return m_CaretSelectPosition + compositionString.Length; }
        set { selectionAnchorPosition = value; selectionFocusPosition = value; }
    }

    /// <summary>
    /// Get: Returns the fixed position of selection
    /// Set: If Input.compositionString is 0 set the fixed position
    /// </summary>

    public int selectionAnchorPosition
    {
        get { return m_CaretPosition + compositionString.Length; }
        set
        {
            if(compositionString.Length != 0)
                return;

            m_CaretPosition = value;
            ClampPos(ref m_CaretPosition);
        }
    }

    /// <summary>
    /// Get: Returns the variable position of selection
    /// Set: If Input.compositionString is 0 set the variable position
    /// </summary>

    public int selectionFocusPosition
    {
        get { return m_CaretSelectPosition + compositionString.Length; }
        set
        {
            if(compositionString.Length != 0)
                return;

            m_CaretSelectPosition = value;
            ClampPos(ref m_CaretSelectPosition);
        }
    }

#if UNITY_EDITOR
    // Remember: This is NOT related to text validation!
    // This is Unity's own OnValidate method which is invoked when changing values in the Inspector.
    protected override void OnValidate()
    {
        base.OnValidate();
        EnforceContentType();
        EnforceTextHOverflow();

        m_CharacterLimit = Math.Max(0, m_CharacterLimit);

        //This can be invoked before OnEnabled is called. So we shouldn't be accessing other objects, before OnEnable is called.
        if(!IsActive())
            return;

        UpdateLabel();
        if(m_AllowInput)
            SetCaretActive();
    }

#endif // if UNITY_EDITOR

    protected override void OnEnable()
    {
        base.OnEnable();
        if(m_Text == null)
            m_Text = string.Empty;
        m_DrawStart = 0;
        m_DrawEnd = m_Text.Length;

        // If we have a cached renderer then we had OnDisable called so just restore the material.
        if(m_CachedInputRenderer != null)
            m_CachedInputRenderer.SetMaterial(m_TextComponent.GetModifiedMaterial(Graphic.defaultGraphicMaterial), Texture2D.whiteTexture);

        if(m_TextComponent != null)
        {
            m_TextComponent.RegisterDirtyVerticesCallback(MarkGeometryAsDirty);
            m_TextComponent.RegisterDirtyVerticesCallback(UpdateLabel);
            m_TextComponent.RegisterDirtyMaterialCallback(UpdateCaretMaterial);
            UpdateLabel();
        }
    }

    protected override void OnDisable()
    {
        // the coroutine will be terminated, so this will ensure it restarts when we are next activated
        m_BlinkCoroutine = null;

        DeactivateInputField();
        if(m_TextComponent != null)
        {
            m_TextComponent.UnregisterDirtyVerticesCallback(MarkGeometryAsDirty);
            m_TextComponent.UnregisterDirtyVerticesCallback(UpdateLabel);
            m_TextComponent.UnregisterDirtyMaterialCallback(UpdateCaretMaterial);
        }
        CanvasUpdateRegistry.UnRegisterCanvasElementForRebuild(this);

        // Clear needs to be called otherwise sync never happens as the object is disabled.
        if(m_CachedInputRenderer != null)
            m_CachedInputRenderer.Clear();

        if(m_Mesh != null)
            DestroyImmediate(m_Mesh);
        m_Mesh = null;

        base.OnDisable();
    }

    IEnumerator CaretBlink()
    {
        // Always ensure caret is initially visible since it can otherwise be confusing for a moment.
        m_CaretVisible = true;
        yield return null;

        while(isFocused && m_CaretBlinkRate > 0)
        {
            // the blink rate is expressed as a frequency
            float blinkPeriod = 1f / m_CaretBlinkRate;

            // the caret should be ON if we are in the first half of the blink period
            bool blinkState = (Time.unscaledTime - m_BlinkStartTime) % blinkPeriod < blinkPeriod / 2;
            if(m_CaretVisible != blinkState)
            {
                m_CaretVisible = blinkState;
                if(!hasSelection)
                    MarkGeometryAsDirty();
            }

            // Then wait again.
            yield return null;
        }
        m_BlinkCoroutine = null;
    }

    void SetCaretVisible()
    {
        if(!m_AllowInput)
            return;

        m_CaretVisible = true;
        m_BlinkStartTime = Time.unscaledTime;
        SetCaretActive();
    }

    // SetCaretActive will not set the caret immediately visible - it will wait for the next time to blink.
    // However, it will handle things correctly if the blink speed changed from zero to non-zero or non-zero to zero.
    void SetCaretActive()
    {
        if(!m_AllowInput)
            return;

        if(m_CaretBlinkRate > 0.0f)
        {
            if(m_BlinkCoroutine == null)
                m_BlinkCoroutine = StartCoroutine(CaretBlink());
        }
        else
        {
            m_CaretVisible = true;
        }
    }

    private void UpdateCaretMaterial()
    {
        if(m_TextComponent != null && m_CachedInputRenderer != null)
            m_CachedInputRenderer.SetMaterial(m_TextComponent.GetModifiedMaterial(Graphic.defaultGraphicMaterial), Texture2D.whiteTexture);
    }

    protected void OnFocus()
    {
        SelectAll();
    }

    protected void SelectAll()
    {
        caretPositionInternal = text.Length;
        caretSelectPositionInternal = 0;
    }

    public void MoveTextEnd(bool shift)
    {
        int position = text.Length;

        if(shift)
        {
            caretSelectPositionInternal = position;
        }
        else
        {
            caretPositionInternal = position;
            caretSelectPositionInternal = caretPositionInternal;
        }
        UpdateLabel();
    }

    public void MoveTextStart(bool shift)
    {
        int position = 0;

        if(shift)
        {
            caretSelectPositionInternal = position;
        }
        else
        {
            caretPositionInternal = position;
            caretSelectPositionInternal = caretPositionInternal;
        }

        UpdateLabel();
    }

    static string clipboard
    {
        get
        {
            return GUIUtility.systemCopyBuffer;
        }
        set
        {
            GUIUtility.systemCopyBuffer = value;
        }
    }

    private bool InPlaceEditing()
    {
        return !TouchScreenKeyboard.isSupported;
    }

    void UpdateCaretFromKeyboard()
    {
        var selectionRange = m_Keyboard.selection;

        var selectionStart = selectionRange.start;
        var selectionEnd = selectionRange.end;

        var caretChanged = false;

        if(caretPositionInternal != selectionStart)
        {
            caretChanged = true;
            caretPositionInternal = selectionStart;
        }

        if(caretSelectPositionInternal != selectionEnd)
        {
            caretSelectPositionInternal = selectionEnd;
            caretChanged = true;
        }

        if(caretChanged)
        {
            m_BlinkStartTime = Time.unscaledTime;

            UpdateLabel();
        }
    }

    /// <summary>
    /// Update the text based on input.
    /// </summary>
    // TODO: Make LateUpdate a coroutine instead. Allows us to control the update to only be when the field is active.
    protected virtual void LateUpdate()
    {
        // Only activate if we are not already activated.
        if(m_ShouldActivateNextUpdate)
        {
            if(!isFocused)
            {
                ActivateInputFieldInternal();
                m_ShouldActivateNextUpdate = false;
                return;
            }

            // Reset as we are already activated.
            m_ShouldActivateNextUpdate = false;
        }

        if(InPlaceEditing() || !isFocused)
            return;

        AssignPositioningIfNeeded();

        if(m_Keyboard == null || m_Keyboard.done)
        {
            if(m_Keyboard != null)
            {
                if(!m_ReadOnly)
                    text = m_Keyboard.text;

                if(m_Keyboard.wasCanceled)
                    m_WasCanceled = true;
            }

            OnDeselect(null);
            return;
        }

        string val = m_Keyboard.text;

        if(m_Text != val)
        {
            if(m_ReadOnly)
            {
                m_Keyboard.text = m_Text;
            }
            else
            {
                m_Text = "";

                for(int i = 0; i < val.Length; ++i)
                {
                    char c = val[i];

                    if(c == '\r' || (int)c == 3)
                        c = '\n';

                    if(onValidateInput != null)
                        c = onValidateInput(m_Text, m_Text.Length, c);
                    else if(characterValidation != CharacterValidation.None)
                        c = Validate(m_Text, m_Text.Length, c);

                    if(lineType == LineType.MultiLineSubmit && c == '\n')
                    {
                        m_Keyboard.text = m_Text;

                        OnDeselect(null);
                        return;
                    }

                    if(c != 0)
                        m_Text += c;
                }

                if(characterLimit > 0 && m_Text.Length > characterLimit)
                    m_Text = m_Text.Substring(0, characterLimit);

                if(m_Keyboard.canGetSelection)
                {
                    UpdateCaretFromKeyboard();
                }
                else
                {
                    caretPositionInternal = caretSelectPositionInternal = m_Text.Length;
                }

                // Set keyboard text before updating label, as we might have changed it with validation
                // and update label will take the old value from keyboard if we don't change it here
                if(m_Text != val)
                    m_Keyboard.text = m_Text;

                SendOnValueChangedAndUpdateLabel();
            }
        }
        else if(m_Keyboard.canGetSelection)
        {
            UpdateCaretFromKeyboard();
        }


        if(m_Keyboard.done)
        {
            if(m_Keyboard.wasCanceled)
                m_WasCanceled = true;

            OnDeselect(null);
        }
    }

    [Obsolete("This function is no longer used. Please use RectTransformUtility.ScreenPointToLocalPointInRectangle() instead.")]
    public Vector2 ScreenToLocal(Vector2 screen)
    {
        var theCanvas = m_TextComponent.canvas;
        if(theCanvas == null)
            return screen;

        Vector3 pos = Vector3.zero;
        if(theCanvas.renderMode == RenderMode.ScreenSpaceOverlay)
        {
            pos = m_TextComponent.transform.InverseTransformPoint(screen);
        }
        else if(theCanvas.worldCamera != null)
        {
            Ray mouseRay = theCanvas.worldCamera.ScreenPointToRay(screen);
            float dist;
            Plane plane = new Plane(m_TextComponent.transform.forward, m_TextComponent.transform.position);
            plane.Raycast(mouseRay, out dist);
            pos = m_TextComponent.transform.InverseTransformPoint(mouseRay.GetPoint(dist));
        }
        return new Vector2(pos.x, pos.y);
    }

    private int GetUnclampedCharacterLineFromPosition(Vector2 pos, TextGenerator generator)
    {
        if(!multiLine)
            return 0;

        // transform y to local scale
        float y = pos.y * m_TextComponent.pixelsPerUnit;
        float lastBottomY = 0.0f;

        for(int i = 0; i < generator.lineCount; ++i)
        {
            float topY = generator.lines[i].topY;
            float bottomY = topY - generator.lines[i].height;

            // pos is somewhere in the leading above this line
            if(y > topY)
            {
                // determine which line we're closer to
                float leading = topY - lastBottomY;
                if(y > topY - 0.5f * leading)
                    return i - 1;
                else
                    return i;
            }

            if(y > bottomY)
                return i;

            lastBottomY = bottomY;
        }

        // Position is after last line.
        return generator.lineCount;
    }

    /// <summary>
    /// Given an input position in local space on the Text return the index for the selection cursor at this position.
    /// </summary>

    protected int GetCharacterIndexFromPosition(Vector2 pos)
    {
        TextGenerator gen = m_TextComponent.cachedTextGenerator;

        if(gen.lineCount == 0)
            return 0;

        int line = GetUnclampedCharacterLineFromPosition(pos, gen);
        if(line < 0)
            return 0;
        if(line >= gen.lines.Count)
            return gen.characterCountVisible;

        int startCharIndex = gen.lines[line].startCharIdx;
        int endCharIndex = GetLineEndPosition(gen, line);

        for(int i = startCharIndex; i < endCharIndex; i++)
        {
            if(i >= gen.characters.Count)
                break;

            UICharInfo charInfo = gen.characters[i];
            Vector2 charPos = charInfo.cursorPos / m_TextComponent.pixelsPerUnit;

            float distToCharStart = pos.x - charPos.x;
            float distToCharEnd = charPos.x + (charInfo.charWidth / m_TextComponent.pixelsPerUnit) - pos.x;
            if(distToCharStart < distToCharEnd)
                return i;
        }

        return endCharIndex;
    }

    private bool MayDrag(PointerEventData eventData)
    {
        return IsActive() &&
            IsInteractable() &&
            eventData.button == PointerEventData.InputButton.Left &&
            m_TextComponent != null &&
            m_Keyboard == null;
    }

    public virtual void OnBeginDrag(PointerEventData eventData)
    {
        if(!MayDrag(eventData))
            return;

        m_UpdateDrag = true;
    }

    public virtual void OnDrag(PointerEventData eventData)
    {
        if(!MayDrag(eventData))
            return;

        Vector2 localMousePos;
        RectTransformUtility.ScreenPointToLocalPointInRectangle(textComponent.rectTransform, eventData.position, eventData.pressEventCamera, out localMousePos);
        caretSelectPositionInternal = GetCharacterIndexFromPosition(localMousePos) + m_DrawStart;
        MarkGeometryAsDirty();

        m_DragPositionOutOfBounds = !RectTransformUtility.RectangleContainsScreenPoint(textComponent.rectTransform, eventData.position, eventData.pressEventCamera);
        if(m_DragPositionOutOfBounds && m_DragCoroutine == null)
            m_DragCoroutine = StartCoroutine(MouseDragOutsideRect(eventData));

        eventData.Use();
    }

    IEnumerator MouseDragOutsideRect(PointerEventData eventData)
    {
        while(m_UpdateDrag && m_DragPositionOutOfBounds)
        {
            Vector2 localMousePos;
            RectTransformUtility.ScreenPointToLocalPointInRectangle(textComponent.rectTransform, eventData.position, eventData.pressEventCamera, out localMousePos);

            Rect rect = textComponent.rectTransform.rect;

            if(multiLine)
            {
                if(localMousePos.y > rect.yMax)
                    MoveUp(true, true);
                else if(localMousePos.y < rect.yMin)
                    MoveDown(true, true);
            }
            else
            {
                if(localMousePos.x < rect.xMin)
                    MoveLeft(true, false);
                else if(localMousePos.x > rect.xMax)
                    MoveRight(true, false);
            }
            UpdateLabel();
            float delay = multiLine ? kVScrollSpeed : kHScrollSpeed;
            yield return new WaitForSecondsRealtime(delay);
        }
        m_DragCoroutine = null;
    }

    public virtual void OnEndDrag(PointerEventData eventData)
    {
        if(!MayDrag(eventData))
            return;

        m_UpdateDrag = false;
    }

    public override void OnPointerDown(PointerEventData eventData)
    {
        if(!MayDrag(eventData))
            return;

        EventSystem.current.SetSelectedGameObject(gameObject, eventData);

        bool hadFocusBefore = m_AllowInput;
        base.OnPointerDown(eventData);

        if(!InPlaceEditing())
        {
            if(m_Keyboard == null || !m_Keyboard.active)
            {
                OnSelect(eventData);
                return;
            }
        }

        // Only set caret position if we didn't just get focus now.
        // Otherwise it will overwrite the select all on focus.
        if(hadFocusBefore)
        {
            Vector2 localMousePos;
            RectTransformUtility.ScreenPointToLocalPointInRectangle(textComponent.rectTransform, eventData.position, eventData.pressEventCamera, out localMousePos);

            caretSelectPositionInternal = caretPositionInternal = GetCharacterIndexFromPosition(localMousePos) + m_DrawStart;
        }
        UpdateLabel();
        eventData.Use();
    }

    protected enum EditState
    {
        Continue,
        Finish
    }

    protected EditState KeyPressed(Event evt)
    {
        var currentEventModifiers = evt.modifiers;
        bool ctrl = SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX ? (currentEventModifiers & EventModifiers.Command) != 0 : (currentEventModifiers & EventModifiers.Control) != 0;
        bool shift = (currentEventModifiers & EventModifiers.Shift) != 0;
        bool alt = (currentEventModifiers & EventModifiers.Alt) != 0;
        bool ctrlOnly = ctrl && !alt && !shift;

        switch(evt.keyCode)
        {
            case KeyCode.Backspace:
                {
                    Backspace();
                    return EditState.Continue;
                }

            case KeyCode.Delete:
                {
                    ForwardSpace();
                    return EditState.Continue;
                }

            case KeyCode.Home:
                {
                    MoveTextStart(shift);
                    return EditState.Continue;
                }

            case KeyCode.End:
                {
                    MoveTextEnd(shift);
                    return EditState.Continue;
                }

            // Select All
            case KeyCode.A:
                {
                    if(ctrlOnly)
                    {
                        SelectAll();
                        return EditState.Continue;
                    }
                    break;
                }

            // Copy
            case KeyCode.C:
                {
                    if(ctrlOnly)
                    {
                        if(inputType != InputType.Password)
                            clipboard = GetSelectedString();
                        else
                            clipboard = "";
                        return EditState.Continue;
                    }
                    break;
                }

            // Paste
            case KeyCode.V:
                {
                    if(ctrlOnly)
                    {
                        Append(clipboard);
                        return EditState.Continue;
                    }
                    break;
                }

            // Cut
            case KeyCode.X:
                {
                    if(ctrlOnly)
                    {
                        if(inputType != InputType.Password)
                            clipboard = GetSelectedString();
                        else
                            clipboard = "";
                        Delete();
                        SendOnValueChangedAndUpdateLabel();
                        return EditState.Continue;
                    }
                    break;
                }

            case KeyCode.LeftArrow:
                {
                    MoveLeft(shift, ctrl);
                    return EditState.Continue;
                }

            case KeyCode.RightArrow:
                {
                    MoveRight(shift, ctrl);
                    return EditState.Continue;
                }

            case KeyCode.UpArrow:
                {
                    MoveUp(shift);
                    return EditState.Continue;
                }

            case KeyCode.DownArrow:
                {
                    MoveDown(shift);
                    return EditState.Continue;
                }

            // Submit
            case KeyCode.Return:
            case KeyCode.KeypadEnter:
                {
                    if(lineType != LineType.MultiLineNewline)
                    {
                        return EditState.Finish;
                    }
                    break;
                }

            case KeyCode.Escape:
                {
                    m_WasCanceled = true;
                    return EditState.Finish;
                }
        }

        char c = evt.character;
        // Don't allow return chars or tabulator key to be entered into single line fields.
        if(!multiLine && (c == '\t' || c == '\r' || c == 10))
            return EditState.Continue;

        // Convert carriage return and end-of-text characters to newline.
        if(c == '\r' || (int)c == 3)
            c = '\n';

        if(IsValidChar(c))
        {
            Append(c);
        }

        if(c == 0)
        {
            if(compositionString.Length > 0)
            {
                UpdateLabel();
            }
        }
        return EditState.Continue;
    }

    private bool IsValidChar(char c)
    {
        // Delete key on mac
        if((int)c == 127)
            return false;
        // Accept newline and tab
        if(c == '\t' || c == '\n')
            return true;

        return m_TextComponent.font.HasCharacter(c);
    }

    /// <summary>
    /// Handle the specified event.
    /// </summary>
    private Event m_ProcessingEvent = new Event();

    public void ProcessEvent(Event e)
    {
        KeyPressed(e);
    }

    public virtual void OnUpdateSelected(BaseEventData eventData)
    {
        if(!isFocused)
            return;

        bool consumedEvent = false;
        while(Event.PopEvent(m_ProcessingEvent))
        {
            if(m_ProcessingEvent.rawType == EventType.KeyDown)
            {
                consumedEvent = true;
                var shouldContinue = KeyPressed(m_ProcessingEvent);
                if(shouldContinue == EditState.Finish)
                {
                    DeactivateInputField();
                    break;
                }
            }

            switch(m_ProcessingEvent.type)
            {
                case EventType.ValidateCommand:
                case EventType.ExecuteCommand:
                    switch(m_ProcessingEvent.commandName)
                    {
                        case "SelectAll":
                            SelectAll();
                            consumedEvent = true;
                            break;
                    }
                    break;
            }
        }

        if(consumedEvent)
            UpdateLabel();

        eventData.Use();
    }

    private string GetSelectedString()
    {
        if(!hasSelection)
            return "";

        int startPos = caretPositionInternal;
        int endPos = caretSelectPositionInternal;

        // Ensure startPos is always less then endPos to make the code simpler
        if(startPos > endPos)
        {
            int temp = startPos;
            startPos = endPos;
            endPos = temp;
        }

        return text.Substring(startPos, endPos - startPos);
    }

    private int FindtNextWordBegin()
    {
        if(caretSelectPositionInternal + 1 >= text.Length)
            return text.Length;

        int spaceLoc = text.IndexOfAny(kSeparators, caretSelectPositionInternal + 1);

        if(spaceLoc == -1)
            spaceLoc = text.Length;
        else
            spaceLoc++;

        return spaceLoc;
    }

    private void MoveRight(bool shift, bool ctrl)
    {
        if(hasSelection && !shift)
        {
            // By convention, if we have a selection and move right without holding shift,
            // we just place the cursor at the end.
            caretPositionInternal = caretSelectPositionInternal = Mathf.Max(caretPositionInternal, caretSelectPositionInternal);
            return;
        }

        int position;
        if(ctrl)
            position = FindtNextWordBegin();
        else
            position = caretSelectPositionInternal + 1;

        if(shift)
            caretSelectPositionInternal = position;
        else
            caretSelectPositionInternal = caretPositionInternal = position;
    }

    private int FindtPrevWordBegin()
    {
        if(caretSelectPositionInternal - 2 < 0)
            return 0;

        int spaceLoc = text.LastIndexOfAny(kSeparators, caretSelectPositionInternal - 2);

        if(spaceLoc == -1)
            spaceLoc = 0;
        else
            spaceLoc++;

        return spaceLoc;
    }

    private void MoveLeft(bool shift, bool ctrl)
    {
        if(hasSelection && !shift)
        {
            // By convention, if we have a selection and move left without holding shift,
            // we just place the cursor at the start.
            caretPositionInternal = caretSelectPositionInternal = Mathf.Min(caretPositionInternal, caretSelectPositionInternal);
            return;
        }

        int position;
        if(ctrl)
            position = FindtPrevWordBegin();
        else
            position = caretSelectPositionInternal - 1;

        if(shift)
            caretSelectPositionInternal = position;
        else
            caretSelectPositionInternal = caretPositionInternal = position;
    }

    private int DetermineCharacterLine(int charPos, TextGenerator generator)
    {
        for(int i = 0; i < generator.lineCount - 1; ++i)
        {
            if(generator.lines[i + 1].startCharIdx > charPos)
                return i;
        }

        return generator.lineCount - 1;
    }

    /// <summary>
    ///  Use cachedInputTextGenerator as the y component for the UICharInfo is not required
    /// </summary>

    private int LineUpCharacterPosition(int originalPos, bool goToFirstChar)
    {
        if(originalPos >= cachedInputTextGenerator.characters.Count)
            return 0;

        UICharInfo originChar = cachedInputTextGenerator.characters[originalPos];
        int originLine = DetermineCharacterLine(originalPos, cachedInputTextGenerator);

        // We are on the first line return first character
        if(originLine <= 0)
            return goToFirstChar ? 0 : originalPos;

        int endCharIdx = cachedInputTextGenerator.lines[originLine].startCharIdx - 1;

        for(int i = cachedInputTextGenerator.lines[originLine - 1].startCharIdx; i < endCharIdx; ++i)
        {
            if(cachedInputTextGenerator.characters[i].cursorPos.x >= originChar.cursorPos.x)
                return i;
        }
        return endCharIdx;
    }

    /// <summary>
    ///  Use cachedInputTextGenerator as the y component for the UICharInfo is not required
    /// </summary>

    private int LineDownCharacterPosition(int originalPos, bool goToLastChar)
    {
        if(originalPos >= cachedInputTextGenerator.characterCountVisible)
            return text.Length;

        UICharInfo originChar = cachedInputTextGenerator.characters[originalPos];
        int originLine = DetermineCharacterLine(originalPos, cachedInputTextGenerator);

        // We are on the last line return last character
        if(originLine + 1 >= cachedInputTextGenerator.lineCount)
            return goToLastChar ? text.Length : originalPos;

        // Need to determine end line for next line.
        int endCharIdx = GetLineEndPosition(cachedInputTextGenerator, originLine + 1);

        for(int i = cachedInputTextGenerator.lines[originLine + 1].startCharIdx; i < endCharIdx; ++i)
        {
            if(cachedInputTextGenerator.characters[i].cursorPos.x >= originChar.cursorPos.x)
                return i;
        }
        return endCharIdx;
    }

    private void MoveDown(bool shift)
    {
        MoveDown(shift, true);
    }

    private void MoveDown(bool shift, bool goToLastChar)
    {
        if(hasSelection && !shift)
        {
            // If we have a selection and press down without shift,
            // set caret position to end of selection before we move it down.
            caretPositionInternal = caretSelectPositionInternal = Mathf.Max(caretPositionInternal, caretSelectPositionInternal);
        }

        int position = multiLine ? LineDownCharacterPosition(caretSelectPositionInternal, goToLastChar) : text.Length;

        if(shift)
            caretSelectPositionInternal = position;
        else
            caretPositionInternal = caretSelectPositionInternal = position;
    }

    private void MoveUp(bool shift)
    {
        MoveUp(shift, true);
    }

    private void MoveUp(bool shift, bool goToFirstChar)
    {
        if(hasSelection && !shift)
        {
            // If we have a selection and press up without shift,
            // set caret position to start of selection before we move it up.
            caretPositionInternal = caretSelectPositionInternal = Mathf.Min(caretPositionInternal, caretSelectPositionInternal);
        }

        int position = multiLine ? LineUpCharacterPosition(caretSelectPositionInternal, goToFirstChar) : 0;

        if(shift)
            caretSelectPositionInternal = position;
        else
            caretSelectPositionInternal = caretPositionInternal = position;
    }

    private void Delete()
    {
        if(m_ReadOnly)
            return;

        if(caretPositionInternal == caretSelectPositionInternal)
            return;

        if(caretPositionInternal < caretSelectPositionInternal)
        {
            m_Text = text.Substring(0, caretPositionInternal) + text.Substring(caretSelectPositionInternal, text.Length - caretSelectPositionInternal);
            caretSelectPositionInternal = caretPositionInternal;
        }
        else
        {
            m_Text = text.Substring(0, caretSelectPositionInternal) + text.Substring(caretPositionInternal, text.Length - caretPositionInternal);
            caretPositionInternal = caretSelectPositionInternal;
        }
    }

    private void ForwardSpace()
    {
        if(m_ReadOnly)
            return;

        if(hasSelection)
        {
            Delete();
            SendOnValueChangedAndUpdateLabel();
        }
        else
        {
            if(caretPositionInternal < text.Length)
            {
                m_Text = text.Remove(caretPositionInternal, 1);
                SendOnValueChangedAndUpdateLabel();
            }
        }
    }

    private void Backspace()
    {
        if(m_ReadOnly)
            return;

        if(hasSelection)
        {
            Delete();
            SendOnValueChangedAndUpdateLabel();
        }
        else
        {
            if(caretPositionInternal > 0)
            {
                m_Text = text.Remove(caretPositionInternal - 1, 1);
                caretSelectPositionInternal = caretPositionInternal = caretPositionInternal - 1;
                SendOnValueChangedAndUpdateLabel();
            }
        }
    }

    // Insert the character and update the label.
    private void Insert(char c)
    {
        if(m_ReadOnly)
            return;

        string replaceString = c.ToString();
        Delete();

        // Can't go past the character limit
        if(characterLimit > 0 && text.Length >= characterLimit)
            return;

        m_Text = text.Insert(m_CaretPosition, replaceString);
        caretSelectPositionInternal = caretPositionInternal += replaceString.Length;

        SendOnValueChanged();
    }

    private void SendOnValueChangedAndUpdateLabel()
    {
        SendOnValueChanged();
        UpdateLabel();
    }

    private void SendOnValueChanged()
    {
        if(onValueChanged != null)
            onValueChanged.Invoke(text);
    }

    private void SendOnFocusChanged()
    {
        if(onFocusChanged != null)
            onFocusChanged.Invoke(m_AllowInput);
    }


    /// <summary>
    /// Submit the input field's text.
    /// </summary>

    protected void SendOnSubmit()
    {
        if(onEndEdit != null)
            onEndEdit.Invoke(m_Text);
    }

    /// <summary>
    /// Append the specified text to the end of the current.
    /// </summary>

    protected virtual void Append(string input)
    {
        if(m_ReadOnly)
            return;

        if(!InPlaceEditing())
            return;

        for(int i = 0, imax = input.Length; i < imax; ++i)
        {
            char c = input[i];

            if(c >= ' ' || c == '\t' || c == '\r' || c == 10 || c == '\n')
            {
                Append(c);
            }
        }
    }

    protected virtual void Append(char input)
    {
        if(m_ReadOnly)
            return;

        if(!InPlaceEditing())
            return;

        // If we have an input validator, validate the input first
        int insertionPoint = Math.Min(selectionFocusPosition, selectionAnchorPosition);
        if(onValidateInput != null)
            input = onValidateInput(text, insertionPoint, input);
        else if(characterValidation != CharacterValidation.None)
            input = Validate(text, insertionPoint, input);

        // If the input is invalid, skip it
        if(input == 0)
            return;

        // Append the character and update the label
        Insert(input);
    }

    /// <summary>
    /// Update the visual text Text.
    /// </summary>

    protected void UpdateLabel()
    {
        if(m_TextComponent != null && m_TextComponent.font != null && !m_PreventFontCallback)
        {
            // TextGenerator.Populate invokes a callback that's called for anything
            // that needs to be updated when the data for that font has changed.
            // This makes all Text components that use that font update their vertices.
            // In turn, this makes the InputField that's associated with that Text component
            // update its label by calling this UpdateLabel method.
            // This is a recursive call we want to prevent, since it makes the InputField
            // update based on font data that didn't yet finish executing, or alternatively
            // hang on infinite recursion, depending on whether the cached value is cached
            // before or after the calculation.
            //
            // This callback also occurs when assigning text to our Text component, i.e.,
            // m_TextComponent.text = processed;

            m_PreventFontCallback = true;

            string fullText;
            if(compositionString.Length > 0)
                fullText = text.Substring(0, m_CaretPosition) + compositionString + text.Substring(m_CaretPosition);
            else
                fullText = text;

            string processed;
            if(inputType == InputType.Password)
                processed = new string(asteriskChar, fullText.Length);
            else
                processed = fullText;

            bool isEmpty = string.IsNullOrEmpty(fullText);

            if(m_Placeholder != null)
                m_Placeholder.enabled = isEmpty;

            // If not currently editing the text, set the visible range to the whole text.
            // The UpdateLabel method will then truncate it to the part that fits inside the Text area.
            // We can't do this when text is being edited since it would discard the current scroll,
            // which is defined by means of the m_DrawStart and m_DrawEnd indices.
            if(!m_AllowInput)
            {
                m_DrawStart = 0;
                m_DrawEnd = m_Text.Length;
            }

            if(!isEmpty)
            {
                // Determine what will actually fit into the given line
                Vector2 extents = m_TextComponent.rectTransform.rect.size;

                var settings = m_TextComponent.GetGenerationSettings(extents);
                settings.generateOutOfBounds = true;

                cachedInputTextGenerator.PopulateWithErrors(processed, settings, gameObject);

                SetDrawRangeToContainCaretPosition(caretSelectPositionInternal);

                if(!m_isSupportEmoji)
                    processed = processed.Substring(m_DrawStart, Mathf.Min(m_DrawEnd, processed.Length) - m_DrawStart);

                SetCaretVisible();
            }
            m_TextComponent.text = processed;
            MarkGeometryAsDirty();
            m_PreventFontCallback = false;
        }
    }

    private bool IsSelectionVisible()
    {
        if(m_DrawStart > caretPositionInternal || m_DrawStart > caretSelectPositionInternal)
            return false;

        if(m_DrawEnd < caretPositionInternal || m_DrawEnd < caretSelectPositionInternal)
            return false;

        return true;
    }

    private static int GetLineStartPosition(TextGenerator gen, int line)
    {
        line = Mathf.Clamp(line, 0, gen.lines.Count - 1);
        return gen.lines[line].startCharIdx;
    }

    private static int GetLineEndPosition(TextGenerator gen, int line)
    {
        line = Mathf.Max(line, 0);
        if(line + 1 < gen.lines.Count)
            return gen.lines[line + 1].startCharIdx - 1;
        return gen.characterCountVisible;
    }

    private void SetDrawRangeToContainCaretPosition(int caretPos)
    {
        // We don't have any generated lines generation is not valid.
        if(cachedInputTextGenerator.lineCount <= 0)
            return;

        // the extents gets modified by the pixel density, so we need to use the generated extents since that will be in the same 'space' as
        // the values returned by the TextGenerator.lines[x].height for instance.
        Vector2 extents = cachedInputTextGenerator.rectExtents.size;

        if(multiLine)
        {
            var lines = cachedInputTextGenerator.lines;
            int caretLine = DetermineCharacterLine(caretPos, cachedInputTextGenerator);

            if(caretPos > m_DrawEnd)
            {
                // Caret comes after drawEnd, so we need to move drawEnd to the end of the line with the caret
                m_DrawEnd = GetLineEndPosition(cachedInputTextGenerator, caretLine);
                float bottomY = lines[caretLine].topY - lines[caretLine].height;
                if(caretLine == lines.Count - 1)
                {
                    // Remove interline spacing on last line.
                    bottomY += lines[caretLine].leading;
                }
                int startLine = caretLine;
                while(startLine > 0)
                {
                    float topY = lines[startLine - 1].topY;
                    if(topY - bottomY > extents.y)
                        break;
                    startLine--;
                }
                m_DrawStart = GetLineStartPosition(cachedInputTextGenerator, startLine);
            }
            else
            {
                if(caretPos < m_DrawStart)
                {
                    // Caret comes before drawStart, so we need to move drawStart to an earlier line start that comes before caret.
                    m_DrawStart = GetLineStartPosition(cachedInputTextGenerator, caretLine);
                }

                int startLine = DetermineCharacterLine(m_DrawStart, cachedInputTextGenerator);
                int endLine = startLine;

                float topY = lines[startLine].topY;
                float bottomY = lines[endLine].topY - lines[endLine].height;

                if(endLine == lines.Count - 1)
                {
                    // Remove interline spacing on last line.
                    bottomY += lines[endLine].leading;
                }

                while(endLine < lines.Count - 1)
                {
                    bottomY = lines[endLine + 1].topY - lines[endLine + 1].height;

                    if(endLine + 1 == lines.Count - 1)
                    {
                        // Remove interline spacing on last line.
                        bottomY += lines[endLine + 1].leading;
                    }

                    if(topY - bottomY > extents.y)
                        break;
                    ++endLine;
                }

                m_DrawEnd = GetLineEndPosition(cachedInputTextGenerator, endLine);

                while(startLine > 0)
                {
                    topY = lines[startLine - 1].topY;
                    if(topY - bottomY > extents.y)
                        break;
                    startLine--;
                }
                m_DrawStart = GetLineStartPosition(cachedInputTextGenerator, startLine);
            }
        }
        else
        {
            var characters = cachedInputTextGenerator.characters;
            if(m_DrawEnd > cachedInputTextGenerator.characterCountVisible)
                m_DrawEnd = cachedInputTextGenerator.characterCountVisible;

            float width = 0.0f;
            if(caretPos > m_DrawEnd || (caretPos == m_DrawEnd && m_DrawStart > 0))
            {
                // fit characters from the caretPos leftward
                m_DrawEnd = caretPos;
                for(m_DrawStart = m_DrawEnd - 1; m_DrawStart >= 0; --m_DrawStart)
                {
                    if(width + characters[m_DrawStart].charWidth > extents.x)
                        break;

                    width += characters[m_DrawStart].charWidth;
                }
                ++m_DrawStart;  // move right one to the last character we could fit on the left
            }
            else
            {
                if(caretPos < m_DrawStart)
                    m_DrawStart = caretPos;

                m_DrawEnd = m_DrawStart;
            }

            // fit characters rightward
            for(; m_DrawEnd < cachedInputTextGenerator.characterCountVisible; ++m_DrawEnd)
            {
                width += characters[m_DrawEnd].charWidth;
                if(width > extents.x)
                    break;
            }
        }
    }

    public void ForceLabelUpdate()
    {
        UpdateLabel();
    }

    private void MarkGeometryAsDirty()
    {
#if UNITY_EDITOR
        if(!Application.isPlaying || UnityEditor.PrefabUtility.GetPrefabObject(gameObject) != null)
            return;
#endif

        CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
    }

    public virtual void Rebuild(CanvasUpdate update)
    {
        switch(update)
        {
            case CanvasUpdate.LatePreRender:
                UpdateGeometry();
                break;
        }
    }

    public virtual void LayoutComplete()
    { }

    public virtual void GraphicUpdateComplete()
    { }

    private void UpdateGeometry()
    {
#if UNITY_EDITOR
        if(!Application.isPlaying)
            return;
#endif
        // No need to draw a cursor on mobile as its handled by the devices keyboard.
        if(!shouldHideMobileInput)
            return;

        if(m_CachedInputRenderer == null && m_TextComponent != null)
        {
            GameObject go = new GameObject(transform.name + " Input Caret", typeof(RectTransform), typeof(CanvasRenderer));
            go.hideFlags = HideFlags.DontSave;
            go.transform.SetParent(m_TextComponent.transform.parent);
            go.transform.SetAsFirstSibling();
            go.layer = gameObject.layer;

            caretRectTrans = go.GetComponent<RectTransform>();
            m_CachedInputRenderer = go.GetComponent<CanvasRenderer>();
            m_CachedInputRenderer.SetMaterial(m_TextComponent.GetModifiedMaterial(Graphic.defaultGraphicMaterial), Texture2D.whiteTexture);

            // Needed as if any layout is present we want the caret to always be the same as the text area.
            go.AddComponent<LayoutElement>().ignoreLayout = true;

            AssignPositioningIfNeeded();
        }

        if(m_CachedInputRenderer == null)
            return;

        OnFillVBO(mesh);
        m_CachedInputRenderer.SetMesh(mesh);
    }

    private void AssignPositioningIfNeeded()
    {
        if(m_TextComponent != null && caretRectTrans != null &&
            (caretRectTrans.localPosition != m_TextComponent.rectTransform.localPosition ||
                caretRectTrans.localRotation != m_TextComponent.rectTransform.localRotation ||
                caretRectTrans.localScale != m_TextComponent.rectTransform.localScale ||
                caretRectTrans.anchorMin != m_TextComponent.rectTransform.anchorMin ||
                caretRectTrans.anchorMax != m_TextComponent.rectTransform.anchorMax ||
                caretRectTrans.anchoredPosition != m_TextComponent.rectTransform.anchoredPosition ||
                caretRectTrans.sizeDelta != m_TextComponent.rectTransform.sizeDelta ||
                caretRectTrans.pivot != m_TextComponent.rectTransform.pivot))
        {
            caretRectTrans.localPosition = m_TextComponent.rectTransform.localPosition;
            caretRectTrans.localRotation = m_TextComponent.rectTransform.localRotation;
            caretRectTrans.localScale = m_TextComponent.rectTransform.localScale;
            caretRectTrans.anchorMin = m_TextComponent.rectTransform.anchorMin;
            caretRectTrans.anchorMax = m_TextComponent.rectTransform.anchorMax;
            caretRectTrans.anchoredPosition = m_TextComponent.rectTransform.anchoredPosition;
            caretRectTrans.sizeDelta = m_TextComponent.rectTransform.sizeDelta;
            caretRectTrans.pivot = m_TextComponent.rectTransform.pivot;
        }
    }

    private void OnFillVBO(Mesh vbo)
    {
        using(var helper = new VertexHelper())
        {
            if(!isFocused)
            {
                helper.FillMesh(vbo);
                return;
            }

            Vector2 roundingOffset = m_TextComponent.PixelAdjustPoint(Vector2.zero);
            if(!hasSelection)
                GenerateCaret(helper, roundingOffset);
            else
                GenerateHightlight(helper, roundingOffset);

            helper.FillMesh(vbo);
        }
    }

    private void GenerateCaret(VertexHelper vbo, Vector2 roundingOffset)
    {
        if(!m_CaretVisible)
            return;

        if(m_CursorVerts == null)
        {
            CreateCursorVerts();
        }

        float width = m_CaretWidth;
        int adjustedPos = Mathf.Max(0, caretPositionInternal - m_DrawStart);
        TextGenerator gen = m_TextComponent.cachedTextGenerator;

        if(gen == null)
            return;

        if(gen.lineCount == 0)
            return;

        Vector2 startPosition = Vector2.zero;

        // Calculate startPosition
        if(adjustedPos < gen.characters.Count)
        {
            UICharInfo cursorChar = gen.characters[adjustedPos];
            startPosition.x = cursorChar.cursorPos.x;
        }
        startPosition.x /= m_TextComponent.pixelsPerUnit;

        // TODO: Only clamp when Text uses horizontal word wrap.
        if(startPosition.x > m_TextComponent.rectTransform.rect.xMax)
            startPosition.x = m_TextComponent.rectTransform.rect.xMax;

        int characterLine = DetermineCharacterLine(adjustedPos, gen);
        startPosition.y = gen.lines[characterLine].topY / m_TextComponent.pixelsPerUnit;
        float height = gen.lines[characterLine].height / m_TextComponent.pixelsPerUnit;

        for(int i = 0; i < m_CursorVerts.Length; i++)
            m_CursorVerts[i].color = caretColor;

        m_CursorVerts[0].position = new Vector3(startPosition.x, startPosition.y - height, 0.0f);
        m_CursorVerts[1].position = new Vector3(startPosition.x + width, startPosition.y - height, 0.0f);
        m_CursorVerts[2].position = new Vector3(startPosition.x + width, startPosition.y, 0.0f);
        m_CursorVerts[3].position = new Vector3(startPosition.x, startPosition.y, 0.0f);

        if(roundingOffset != Vector2.zero)
        {
            for(int i = 0; i < m_CursorVerts.Length; i++)
            {
                UIVertex uiv = m_CursorVerts[i];
                uiv.position.x += roundingOffset.x;
                uiv.position.y += roundingOffset.y;
            }
        }

        vbo.AddUIVertexQuad(m_CursorVerts);

        int screenHeight = Screen.height;
        // Multiple display support only when not the main display. For display 0 the reported
        // resolution is always the desktops resolution since its part of the display API,
        // so we use the standard none multiple display method. (case 741751)
        int displayIndex = m_TextComponent.canvas.targetDisplay;
        if(displayIndex > 0 && displayIndex < Display.displays.Length)
            screenHeight = Display.displays[displayIndex].renderingHeight;

        startPosition.y = screenHeight - startPosition.y;
        input.compositionCursorPos = startPosition;
    }

    private void CreateCursorVerts()
    {
        m_CursorVerts = new UIVertex[4];

        for(int i = 0; i < m_CursorVerts.Length; i++)
        {
            m_CursorVerts[i] = UIVertex.simpleVert;
            m_CursorVerts[i].uv0 = Vector2.zero;
        }
    }

    private void GenerateHightlight(VertexHelper vbo, Vector2 roundingOffset)
    {
        int startChar = Mathf.Max(0, caretPositionInternal - m_DrawStart);
        int endChar = Mathf.Max(0, caretSelectPositionInternal - m_DrawStart);

        // Ensure pos is always less then selPos to make the code simpler
        if(startChar > endChar)
        {
            int temp = startChar;
            startChar = endChar;
            endChar = temp;
        }

        endChar -= 1;
        TextGenerator gen = m_TextComponent.cachedTextGenerator;

        if(gen.lineCount <= 0)
            return;

        int currentLineIndex = DetermineCharacterLine(startChar, gen);

        int lastCharInLineIndex = GetLineEndPosition(gen, currentLineIndex);

        UIVertex vert = UIVertex.simpleVert;
        vert.uv0 = Vector2.zero;
        vert.color = selectionColor;

        int currentChar = startChar;
        while(currentChar <= endChar && currentChar < gen.characterCount)
        {
            if(currentChar == lastCharInLineIndex || currentChar == endChar)
            {
                UICharInfo startCharInfo = gen.characters[startChar];
                UICharInfo endCharInfo = gen.characters[currentChar];
                Vector2 startPosition = new Vector2(startCharInfo.cursorPos.x / m_TextComponent.pixelsPerUnit, gen.lines[currentLineIndex].topY / m_TextComponent.pixelsPerUnit);
                Vector2 endPosition = new Vector2((endCharInfo.cursorPos.x + endCharInfo.charWidth) / m_TextComponent.pixelsPerUnit, startPosition.y - gen.lines[currentLineIndex].height / m_TextComponent.pixelsPerUnit);

                // Checking xMin as well due to text generator not setting position if char is not rendered.
                if(endPosition.x > m_TextComponent.rectTransform.rect.xMax || endPosition.x < m_TextComponent.rectTransform.rect.xMin)
                    endPosition.x = m_TextComponent.rectTransform.rect.xMax;

                var startIndex = vbo.currentVertCount;
                vert.position = new Vector3(startPosition.x, endPosition.y, 0.0f) + (Vector3)roundingOffset;
                vbo.AddVert(vert);

                vert.position = new Vector3(endPosition.x, endPosition.y, 0.0f) + (Vector3)roundingOffset;
                vbo.AddVert(vert);

                vert.position = new Vector3(endPosition.x, startPosition.y, 0.0f) + (Vector3)roundingOffset;
                vbo.AddVert(vert);

                vert.position = new Vector3(startPosition.x, startPosition.y, 0.0f) + (Vector3)roundingOffset;
                vbo.AddVert(vert);

                vbo.AddTriangle(startIndex, startIndex + 1, startIndex + 2);
                vbo.AddTriangle(startIndex + 2, startIndex + 3, startIndex + 0);

                startChar = currentChar + 1;
                currentLineIndex++;

                lastCharInLineIndex = GetLineEndPosition(gen, currentLineIndex);
            }
            currentChar++;
        }
    }

    /// <summary>
    /// Validate the specified input.
    /// </summary>

    protected char Validate(string text, int pos, char ch)
    {
        // Validation is disabled
        if(characterValidation == CharacterValidation.None || !enabled)
            return ch;

        if(characterValidation == CharacterValidation.Integer || characterValidation == CharacterValidation.Decimal)
        {
            // Integer and decimal
            bool cursorBeforeDash = (pos == 0 && text.Length > 0 && text[0] == '-');
            bool selectionAtStart = caretPositionInternal == 0 || caretSelectPositionInternal == 0;
            if(!cursorBeforeDash)
            {
                if(ch >= '0' && ch <= '9') return ch;
                if(ch == '-' && (pos == 0 || selectionAtStart)) return ch;
                if(ch == '.' && characterValidation == CharacterValidation.Decimal && !text.Contains(".")) return ch;
            }
        }
        else if(characterValidation == CharacterValidation.Alphanumeric)
        {
            // All alphanumeric characters
            if(ch >= 'A' && ch <= 'Z') return ch;
            if(ch >= 'a' && ch <= 'z') return ch;
            if(ch >= '0' && ch <= '9') return ch;
        }
        else if(characterValidation == CharacterValidation.Name)
        {
            // FIXME: some actions still lead to invalid input:
            //        - Hitting delete in front of an uppercase letter
            //        - Selecting an uppercase letter and deleting it
            //        - Typing some text, hitting Home and typing more text (we then have an uppercase letter in the middle of a word)
            //        - Typing some text, hitting Home and typing a space (we then have a leading space)
            //        - Erasing a space between two words (we then have an uppercase letter in the middle of a word)
            //        - We accept a trailing space
            //        - We accept the insertion of a space between two lowercase letters.
            //        - Typing text in front of an existing uppercase letter
            //        - ... and certainly more
            //
            // The rule we try to implement are too complex for this kind of verification.

            if(char.IsLetter(ch))
            {
                // Character following a space should be in uppercase.
                if(char.IsLower(ch) && ((pos == 0) || (text[pos - 1] == ' ')))
                {
                    return char.ToUpper(ch);
                }

                // Character not following a space or an apostrophe should be in lowercase.
                if(char.IsUpper(ch) && (pos > 0) && (text[pos - 1] != ' ') && (text[pos - 1] != '\''))
                {
                    return char.ToLower(ch);
                }

                return ch;
            }

            if(ch == '\'')
            {
                // Don't allow more than one apostrophe
                if(!text.Contains("'"))
                    // Don't allow consecutive spaces and apostrophes.
                    if(!(((pos > 0) && ((text[pos - 1] == ' ') || (text[pos - 1] == '\''))) ||
                            ((pos < text.Length) && ((text[pos] == ' ') || (text[pos] == '\'')))))
                        return ch;
            }

            if(ch == ' ')
            {
                // Don't allow consecutive spaces and apostrophes.
                if(!(((pos > 0) && ((text[pos - 1] == ' ') || (text[pos - 1] == '\''))) ||
                        ((pos < text.Length) && ((text[pos] == ' ') || (text[pos] == '\'')))))
                    return ch;
            }
        }
        else if(characterValidation == CharacterValidation.EmailAddress)
        {
            // From StackOverflow about allowed characters in email addresses:
            // Uppercase and lowercase English letters (a-z, A-Z)
            // Digits 0 to 9
            // Characters ! # $ % & ' * + - / = ? ^ _ ` { | } ~
            // Character . (dot, period, full stop) provided that it is not the first or last character,
            // and provided also that it does not appear two or more times consecutively.

            if(ch >= 'A' && ch <= 'Z') return ch;
            if(ch >= 'a' && ch <= 'z') return ch;
            if(ch >= '0' && ch <= '9') return ch;
            if(ch == '@' && text.IndexOf('@') == -1) return ch;
            if(kEmailSpecialCharacters.IndexOf(ch) != -1) return ch;
            if(ch == '.')
            {
                char lastChar = (text.Length > 0) ? text[Mathf.Clamp(pos, 0, text.Length - 1)] : ' ';
                char nextChar = (text.Length > 0) ? text[Mathf.Clamp(pos + 1, 0, text.Length - 1)] : '\n';
                if(lastChar != '.' && nextChar != '.')
                    return ch;
            }
        }
        return (char)0;
    }

    public void ActivateInputField()
    {
        if(m_TextComponent == null || m_TextComponent.font == null || !IsActive() || !IsInteractable())
            return;

        if(isFocused)
        {
            if(m_Keyboard != null && !m_Keyboard.active)
            {
                m_Keyboard.active = true;
                m_Keyboard.text = m_Text;
            }
        }

        m_ShouldActivateNextUpdate = true;
    }

    private void ActivateInputFieldInternal()
    {
        if(EventSystem.current == null)
            return;

        if(EventSystem.current.currentSelectedGameObject != gameObject)
            EventSystem.current.SetSelectedGameObject(gameObject);

        if(TouchScreenKeyboard.isSupported)
        {
            if(input.touchSupported)
            {
                TouchScreenKeyboard.hideInput = shouldHideMobileInput;
            }

            m_Keyboard = (inputType == InputType.Password) ?
                TouchScreenKeyboard.Open(m_Text, keyboardType, false, multiLine, true) :
                TouchScreenKeyboard.Open(m_Text, keyboardType, inputType == InputType.AutoCorrect, multiLine);

            // Mimics OnFocus but as mobile doesn't properly support select all
            // just set it to the end of the text (where it would move when typing starts)
            MoveTextEnd(false);
        }
        else
        {
            input.imeCompositionMode = IMECompositionMode.On;
            OnFocus();
        }

        m_AllowInput = true;
        m_OriginalText = text;
        m_WasCanceled = false;
        SetCaretVisible();
        UpdateLabel();
        SendOnFocusChanged();
    }

    public override void OnSelect(BaseEventData eventData)
    {
        base.OnSelect(eventData);

        if(shouldActivateOnSelect)
            ActivateInputField();
    }

    public virtual void OnPointerClick(PointerEventData eventData)
    {
        if(eventData.button != PointerEventData.InputButton.Left)
            return;

        ActivateInputField();
    }

    public void DeactivateInputField()
    {
        // Not activated do nothing.
        if(!m_AllowInput)
            return;

        m_HasDoneFocusTransition = false;
        m_AllowInput = false;

        if(m_Placeholder != null)
            m_Placeholder.enabled = string.IsNullOrEmpty(m_Text);

        if(m_TextComponent != null && IsInteractable())
        {
            if(m_WasCanceled)
                text = m_OriginalText;

            if(m_Keyboard != null)
            {
                m_Keyboard.active = false;
                m_Keyboard = null;
            }

            m_CaretPosition = m_CaretSelectPosition = 0;

            SendOnSubmit();

            input.imeCompositionMode = IMECompositionMode.Auto;
        }

        MarkGeometryAsDirty();
        SendOnFocusChanged();
    }

    public override void OnDeselect(BaseEventData eventData)
    {
        DeactivateInputField();
        base.OnDeselect(eventData);
    }

    public virtual void OnSubmit(BaseEventData eventData)
    {
        if(!IsActive() || !IsInteractable())
            return;

        if(!isFocused)
            m_ShouldActivateNextUpdate = true;
    }

    private void EnforceContentType()
    {
        switch(contentType)
        {
            case ContentType.Standard:
                {
                    // Don't enforce line type for this content type.
                    m_InputType = InputType.Standard;
                    m_KeyboardType = TouchScreenKeyboardType.Default;
                    m_CharacterValidation = CharacterValidation.None;
                    break;
                }
            case ContentType.Autocorrected:
                {
                    // Don't enforce line type for this content type.
                    m_InputType = InputType.AutoCorrect;
                    m_KeyboardType = TouchScreenKeyboardType.Default;
                    m_CharacterValidation = CharacterValidation.None;
                    break;
                }
            case ContentType.IntegerNumber:
                {
                    m_LineType = LineType.SingleLine;
                    m_InputType = InputType.Standard;
                    m_KeyboardType = TouchScreenKeyboardType.NumberPad;
                    m_CharacterValidation = CharacterValidation.Integer;
                    break;
                }
            case ContentType.DecimalNumber:
                {
                    m_LineType = LineType.SingleLine;
                    m_InputType = InputType.Standard;
                    m_KeyboardType = TouchScreenKeyboardType.NumbersAndPunctuation;
                    m_CharacterValidation = CharacterValidation.Decimal;
                    break;
                }
            case ContentType.Alphanumeric:
                {
                    m_LineType = LineType.SingleLine;
                    m_InputType = InputType.Standard;
                    m_KeyboardType = TouchScreenKeyboardType.ASCIICapable;
                    m_CharacterValidation = CharacterValidation.Alphanumeric;
                    break;
                }
            case ContentType.Name:
                {
                    m_LineType = LineType.SingleLine;
                    m_InputType = InputType.Standard;
                    m_KeyboardType = TouchScreenKeyboardType.Default;
                    m_CharacterValidation = CharacterValidation.Name;
                    break;
                }
            case ContentType.EmailAddress:
                {
                    m_LineType = LineType.SingleLine;
                    m_InputType = InputType.Standard;
                    m_KeyboardType = TouchScreenKeyboardType.EmailAddress;
                    m_CharacterValidation = CharacterValidation.EmailAddress;
                    break;
                }
            case ContentType.Password:
                {
                    m_LineType = LineType.SingleLine;
                    m_InputType = InputType.Password;
                    m_KeyboardType = TouchScreenKeyboardType.Default;
                    m_CharacterValidation = CharacterValidation.None;
                    break;
                }
            case ContentType.Pin:
                {
                    m_LineType = LineType.SingleLine;
                    m_InputType = InputType.Password;
                    m_KeyboardType = TouchScreenKeyboardType.NumberPad;
                    m_CharacterValidation = CharacterValidation.Integer;
                    break;
                }
            default:
                {
                    // Includes Custom type. Nothing should be enforced.
                    break;
                }
        }

        EnforceTextHOverflow();
    }

    void EnforceTextHOverflow()
    {
        if(m_TextComponent != null)
            if(multiLine)
                m_TextComponent.horizontalOverflow = HorizontalWrapMode.Wrap;
            else
                m_TextComponent.horizontalOverflow = HorizontalWrapMode.Overflow;
    }

    void SetToCustomIfContentTypeIsNot(params ContentType[] allowedContentTypes)
    {
        if(contentType == ContentType.Custom)
            return;

        for(int i = 0; i < allowedContentTypes.Length; i++)
            if(contentType == allowedContentTypes[i])
                return;

        contentType = ContentType.Custom;
    }

    void SetToCustom()
    {
        if(contentType == ContentType.Custom)
            return;

        contentType = ContentType.Custom;
    }

    protected override void DoStateTransition(SelectionState state, bool instant)
    {
        if(m_HasDoneFocusTransition)
            state = SelectionState.Highlighted;
        else if(state == SelectionState.Pressed)
            m_HasDoneFocusTransition = true;

        base.DoStateTransition(state, instant);
    }

    public virtual void CalculateLayoutInputHorizontal() { }
    public virtual void CalculateLayoutInputVertical() { }

    public virtual float minWidth { get { return 0; } }

    public virtual float preferredWidth
    {
        get
        {
            if(textComponent == null)
                return 0;
            var settings = textComponent.GetGenerationSettings(Vector2.zero);
            return textComponent.cachedTextGeneratorForLayout.GetPreferredWidth(m_Text, settings) / textComponent.pixelsPerUnit;
        }
    }
    public virtual float flexibleWidth { get { return -1; } }
    public virtual float minHeight { get { return 0; } }

    public virtual float preferredHeight
    {
        get
        {
            if(textComponent == null)
                return 0;
            var settings = textComponent.GetGenerationSettings(new Vector2(textComponent.rectTransform.rect.size.x, 0.0f));
            return textComponent.cachedTextGeneratorForLayout.GetPreferredHeight(m_Text, settings) / textComponent.pixelsPerUnit;
        }
    }

    public virtual float flexibleHeight { get { return -1; } }
    public virtual int layoutPriority { get { return 1; } }

    /// <summary>
    /// 修复 TouchScreenKeyboard bug，unity官方的问题
    /// </summary>
    /// <param name="focus"></param>
    protected virtual void OnApplicationFocus(bool focus)
    {
        if(focus)
        {
            DeactivateInputField();
        }
    }
}

internal static class SetPropertyUtility
{
    public static bool SetColor(ref Color currentValue, Color newValue)
    {
        if(currentValue.r == newValue.r && currentValue.g == newValue.g && currentValue.b == newValue.b && currentValue.a == newValue.a)
            return false;

        currentValue = newValue;
        return true;
    }

    public static bool SetStruct<T>(ref T currentValue, T newValue) where T : struct
    {
        if(currentValue.Equals(newValue))
            return false;

        currentValue = newValue;
        return true;
    }

    public static bool SetClass<T>(ref T currentValue, T newValue) where T : class
    {
        if((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))
            return false;

        currentValue = newValue;
        return true;
    }
}
